Skip to content

refactor: deterministic state-file key ordering#15

Merged
dhruva-reddy merged 1 commit intomainfrom
dhruva-reddy/refactor/state-file-key-order
May 2, 2026
Merged

refactor: deterministic state-file key ordering#15
dhruva-reddy merged 1 commit intomainfrom
dhruva-reddy/refactor/state-file-key-order

Conversation

@dhruva-reddy
Copy link
Copy Markdown
Contributor

ELI5

Problem. Every push rewrites .vapi-state.<env>.json. JavaScript's
JSON.stringify keeps whatever order keys happened to land in — and
state sections get rebuilt from multiple sources (push, pull, bootstrap)
with unpredictable insertion order. Result: about half of every state
diff is just lines moving up and down without any actual change.
Reviewers stopped reading state diffs because they were mostly noise,
which defeats the point of versioning the file.

What this fix does. Adds a sortedKeysReplacer that runs during
JSON.stringify and emits object keys alphabetically at every nesting
level. Arrays stay in their original order (squad member ordering, tool
destination priority, etc. are semantic). State writes go through this
replacer.

Outcome you'll notice. The first push after this lands produces a
big one-time diff of pure reordering across every customer. That's
the cost of landing the fix — please don't read the first state diff
post-merge, it's churn. Every diff after that shows only real changes:
new UUIDs, removed entries, hashes changing. Reviewing state files
becomes useful again.


JS's JSON.stringify honors insertion order. State sections get rebuilt
from multiple sources (push, pull, bootstrap) with unpredictable
insertion order, so ~half of every state-file diff is pure reorderings
that hide the real changes.

  • src/state-serialize.ts (NEW): sortedKeysReplacer (recursive alphabetical
    key sort, arrays untouched) + canonicalize (also drops null/undefined
    leaves; reused by Stack F/G). Kept config-free so tests can import
    without triggering config.ts's CLI parser.
  • src/state.ts: saveState now passes sortedKeysReplacer to JSON.stringify.
    Atomic-write pattern preserved.
  • tests/state-key-order.test.ts: pin byte-identical serialization across
    insertion orders, recursion, array preservation, primitive handling,
    idempotence.

Closes improvements.md #17.

🤖 Generated with Claude Code

dhruva-reddy added a commit that referenced this pull request May 1, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@dhruva-reddy dhruva-reddy force-pushed the dhruva-reddy/refactor/state-file-key-order branch from 898200a to 0f35c9e Compare May 1, 2026 22:56
@dhruva-reddy dhruva-reddy force-pushed the dhruva-reddy/docs/improvements-log branch from 6cd7e2e to 28aa4c9 Compare May 1, 2026 22:56
@dhruva-reddy dhruva-reddy changed the base branch from dhruva-reddy/docs/improvements-log to graphite-base/15 May 2, 2026 01:20
@dhruva-reddy dhruva-reddy force-pushed the dhruva-reddy/refactor/state-file-key-order branch from 0f35c9e to 6430703 Compare May 2, 2026 01:20
@graphite-app graphite-app Bot changed the base branch from graphite-base/15 to main May 2, 2026 01:21
## ELI5

**Problem.** Every push rewrites `.vapi-state.<env>.json`. JavaScript's
`JSON.stringify` keeps whatever order keys happened to land in — and
state sections get rebuilt from multiple sources (push, pull, bootstrap)
with unpredictable insertion order. Result: about half of every state
diff is just lines moving up and down without any actual change.
Reviewers stopped reading state diffs because they were mostly noise,
which defeats the point of versioning the file.

**What this fix does.** Adds a `sortedKeysReplacer` that runs during
`JSON.stringify` and emits object keys alphabetically at every nesting
level. Arrays stay in their original order (squad member ordering, tool
destination priority, etc. are semantic). State writes go through this
replacer.

**Outcome you'll notice.** The first push after this lands produces a
**big one-time diff** of pure reordering across every customer. That's
the cost of landing the fix — please don't read the first state diff
post-merge, it's churn. Every diff after that shows only real changes:
new UUIDs, removed entries, hashes changing. Reviewing state files
becomes useful again.

---

JS's JSON.stringify honors insertion order. State sections get rebuilt
from multiple sources (push, pull, bootstrap) with unpredictable
insertion order, so ~half of every state-file diff is pure reorderings
that hide the real changes.

- src/state-serialize.ts (NEW): sortedKeysReplacer (recursive alphabetical
  key sort, arrays untouched) + canonicalize (also drops null/undefined
  leaves; reused by Stack F/G). Kept config-free so tests can import
  without triggering config.ts's CLI parser.
- src/state.ts: saveState now passes sortedKeysReplacer to JSON.stringify.
  Atomic-write pattern preserved.
- tests/state-key-order.test.ts: pin byte-identical serialization across
  insertion orders, recursion, array preservation, primitive handling,
  idempotence.

Closes improvements.md #17.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@dhruva-reddy dhruva-reddy force-pushed the dhruva-reddy/refactor/state-file-key-order branch from 6430703 to 2fc1864 Compare May 2, 2026 01:21
dhruva-reddy added a commit that referenced this pull request May 2, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@dhruva-reddy dhruva-reddy merged commit c3c1c8a into main May 2, 2026
1 check passed
Copy link
Copy Markdown
Contributor Author

Merge activity

dhruva-reddy added a commit that referenced this pull request May 2, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
dhruva-reddy added a commit that referenced this pull request May 2, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
dhruva-reddy added a commit that referenced this pull request May 5, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
dhruva-reddy added a commit that referenced this pull request May 5, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
dhruva-reddy added a commit that referenced this pull request May 5, 2026
## ELI5

**Problem.** Even when you ran a *scoped* push — say
`npm run push -- <env> assistants/foo.md` to update one assistant —
the engine rewrote the **entire** state file. Any pre-existing drift
in unrelated state entries (UUIDs from earlier sessions, untracked
local files, etc.) swept into the focused commit. Reviewers couldn't
tell from the state-file diff "what did this push actually change?"
and the state file became a pile of side effects accumulated across
sessions instead of a precise record of intent.

**What this fix does.** During a push, the engine tracks which
`resourceId`s it actually mutated (a per-section `Set<string>`). At
end-of-run, for **scoped pushes only**, it loads the on-disk state
fresh, replaces only the touched entries with the in-memory version,
and leaves everything else alone. Full pushes (no scope) still write
wholesale (existing behavior). Credentials are always replaced
because bootstrap pull populates them every push regardless.

This depends on Stack F's `ResourceState` because we need per-entry
metadata to distinguish "stale" from "just-not-touched."

**Outcome you'll notice.** A one-file `npm run push` produces a
one-file diff in the state file — same scope as the resource change.
Reviewers can read the state diff and tell "this push updated
assistant `foo`, here's its new hash" cleanly. Pre-existing drift
elsewhere in state stays where it is until you explicitly address it.

---

When push is scoped to specific paths, only update state entries for
the resources actually touched. A surgical push of two files used to
rewrite the entire state file, sweeping in pre-existing drift from
earlier pushes (improvements.md #15) and producing noisy diffs that
hide the actual scope of the change.

Files:
- src/state-merge.ts (NEW): mergeScoped(disk, inMemory, touched).
  For each section, replace only touched.X resourceIds with the in-memory
  version; leave the rest of disk's section as-is. Credentials are
  always replaced wholesale (bootstrap pull populates them on every
  push). Pure data, no I/O — safe to test directly.
- src/push.ts: TouchedSets tracker. Each upsertState call site
  records the resourceId. End-of-run, partial pushes call
  mergeScoped(loadState(), state, touched) before saveState; full
  pushes save wholesale (existing behavior).
- tests/state-merge.test.ts: replace-only-touched, leave-untouched,
  drift in untouched stays, credentials always replaced.

Closes improvements.md #15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
dhruva-reddy added a commit that referenced this pull request May 10, 2026
…identifier refs from comments

Code-review follow-ups on PR #23:

- src/dep-dedup.ts: replace `Record<string, unknown>` + `as`-cast with a
  named `NameablePayload = { name?: unknown; function?: unknown }` shape
  and `in`-operator narrowing. No casts, no laundered types — the function
  reads two known paths and narrows them at use.

- src/push.ts: scrub "Gap #10" / "Stack J" / "improvements.md #15"
  identifiers from comments. These were internal stack/log markers that
  don't help anyone reading the code; rephrased in domain language while
  keeping the rationale. Also drops redundant `as Record<string, unknown>`
  casts at call sites and reuses `extractResourceName` for the display-name
  fallback in dedup warnings.

- src/push.ts: extend dedup to assistants. The squad → assistant
  auto-apply path (`ensureAssistantExists`) had the same bug class as
  tools / SOs — bootstrap pull stores assistants under `<slug>-<uuid8>`
  keys, and a squad referencing the original local key would mint a
  duplicate assistant on every push. Adds `getExistingRemoteAssistants`
  lazy-fetch + dedup branch with the same orphan-deletion guard and
  apply-via-PATCH flow already in place for tools / SOs. Documents in
  the DependencyContext comment why simulations / personalities /
  scenarios / sim-suites are NOT covered: they're not auto-applied as
  dependencies anywhere in the engine, so the bug class doesn't fire.

- tests/dep-dedup.test.ts: add explicit assistant-payload test
  (top-level `name`, no nested `function`).

Build clean, 115/115 tests pass (was 114, +1 new assistant test).
dhruva-reddy added a commit that referenced this pull request May 10, 2026
…10) (#23)

* fix: dedup dependency auto-apply to prevent duplicate tool mints (Gap #10)

Targeted assistant pushes minted duplicate dashboard tools when bootstrap
pull stored an existing dashboard tool under a name-slugged state key
(e.g. `end-call-67aea057`) instead of the user's original local key. The
exact-key lookup in `ensureToolExists` / `ensureStructuredOutputExists`
missed and POSTed a fresh duplicate. Each subsequent targeted push
repeated the cycle, accumulating dashboard orphans.

This adds a name-based dedup check between the exact-key short-circuit
and the create path, in two layers:

  1. State-side: scan state for any key whose `extractBaseSlug` matches
     the local payload's slugified name (handles bootstrap-renamed keys).
  2. Dashboard-side: lazy-fetch the live `/tool` (and `/structured-output`)
     list once per push and check for a remote resource with the same
     canonical name.

When >1 distinct UUID matches the same name (real on-dashboard duplicates
from prior bug runs), pick the lex-smallest UUID for stable adoption,
warn naming the loser UUIDs, and point at `npm run cleanup`. Never mint
another duplicate.

Adoption flow:
  - Re-key state to the adopted UUID under the local resourceId.
  - Drop other state keys pointing at the same UUID and mark them
    touched, so a subsequent full push doesn't orphan-delete the
    adopted dashboard resource (Stack J / mergeScoped flushes the
    deletion).
  - Route through `applyTool`/`applyStructuredOutput` so the local
    payload PATCHes the dashboard with the standard drift-check flow,
    instead of recording a fake `lastPushedHash` that would silently
    drop a locally-edited dependency.

Tests: 12 unit tests for `findExistingResourceByName` covering state-only,
dashboard-only, both-agree, ambiguous (state-vs-state, state-vs-dashboard),
no-name, exact-key-excluded, no-match. All 114 suites pass.

Refs: improvements.md §10

* docs(improvements): mark §10 as resolved (#23)

* docs: surface tool/SO dedup behavior in learnings; align AGENTS.md / CLAUDE.md

- src/dep-dedup.ts: drop "Gap #10" issue marker from the file header (it
  rots; the rationale is what matters, not the tracker reference).
- docs/learnings/tools.md: new section "Renaming a tool file is safe — the
  engine dedups by `function.name`" — explains the auto-apply dedup safety
  net, the 🔁 / ⚠️ log line semantics, and the cleanup path. Counterpart in
  docs/learnings/structured-outputs.md cross-references it.
- AGENTS.md: add `outbound-campaigns.md` to the Learnings & recipes table
  (was missing); refresh the docs/learnings/ tree in the Project Structure
  section to be complete; add an explicit "Where new knowledge goes" table
  pinning the convention (per-resource tips → docs/learnings/<topic>.md;
  engine-friction log → improvements.md; rationale → code comments;
  onboarding → README.md).
- CLAUDE.md: sync the Required Reading Order list with AGENTS.md's table
  (was missing voice-providers, outbound-agents, outbound-campaigns,
  voicemail-detection); add a brief "Where new knowledge goes" reminder
  pointing back at AGENTS.md as the canonical convention table.

No source behavior changes. Build clean, 114/114 tests pass.

* refactor: address review — extend dedup to assistants, drop internal identifier refs from comments

Code-review follow-ups on PR #23:

- src/dep-dedup.ts: replace `Record<string, unknown>` + `as`-cast with a
  named `NameablePayload = { name?: unknown; function?: unknown }` shape
  and `in`-operator narrowing. No casts, no laundered types — the function
  reads two known paths and narrows them at use.

- src/push.ts: scrub "Gap #10" / "Stack J" / "improvements.md #15"
  identifiers from comments. These were internal stack/log markers that
  don't help anyone reading the code; rephrased in domain language while
  keeping the rationale. Also drops redundant `as Record<string, unknown>`
  casts at call sites and reuses `extractResourceName` for the display-name
  fallback in dedup warnings.

- src/push.ts: extend dedup to assistants. The squad → assistant
  auto-apply path (`ensureAssistantExists`) had the same bug class as
  tools / SOs — bootstrap pull stores assistants under `<slug>-<uuid8>`
  keys, and a squad referencing the original local key would mint a
  duplicate assistant on every push. Adds `getExistingRemoteAssistants`
  lazy-fetch + dedup branch with the same orphan-deletion guard and
  apply-via-PATCH flow already in place for tools / SOs. Documents in
  the DependencyContext comment why simulations / personalities /
  scenarios / sim-suites are NOT covered: they're not auto-applied as
  dependencies anywhere in the engine, so the bug class doesn't fire.

- tests/dep-dedup.test.ts: add explicit assistant-payload test
  (top-level `name`, no nested `function`).

Build clean, 115/115 tests pass (was 114, +1 new assistant test).

* refactor: scrub internal stack/issue identifiers and customer references

Two cleanup sweeps prompted by review feedback on PR #23:

Sweep 1 — internal stack/log identifiers in code comments. References
to "Stack F/G/H/I/J" and "improvements.md #N" are internal-only and
mean nothing to a customer reading the code. Each comment is rephrased
in domain language while preserving the WHY:

  - Stack F → "per-resource content-hash state schema" / "schema migration"
  - Stack G → "drift detection layer"
  - Stack H → "snapshot-on-push for rollback"
  - Stack I → "ETag-based optimistic concurrency"
  - Stack J → "scoped state writes"
  - improvements.md #N → dropped entirely (the rationale stands on its own)

Touched: src/api.ts, cleanup.ts, dep-dedup.ts, drift.ts, pull.ts, push.ts,
resolver.ts, sim-cmd.ts, sim.ts, snapshot.ts, state-merge.ts,
state-serialize.ts, types.ts.

Sweep 2 — customer-specific identifiers in docs/learnings. Customer
brand names (`iForm`, `Mudflap`) and internal ticket IDs
(`PRISM-481`, `PRISM-528`, `PRISM-474`) replaced with generic
placeholders so the public template doesn't carry customer artifacts:

  - iForm → Acme Logistics (in scenario examples)
  - Mudflap → "a customer rollout" (in cross-references)
  - PRISM-* tickets → dropped entirely
  - handoffToiFormSales → handoffToAcmeSales
  - b2b-invoice-end-call.yml → intake-end-call.yml (in renaming example)

Touched: docs/learnings/assistants.md, simulations.md, squads.md, tools.md,
voice-providers.md.

No source-behavior changes. Build clean, 115/115 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants